#!/usr/bin/env ruby
# encoding: utf-8 
#======================================================================================================================#

require 'rubygems'
require 'fattr'
require 'io/console'
require 'SecureRandom'
require 'PrettyComment'
require 'trollop'
require 'terminfo'
require 'tempfile'
require 'yaml'

#YAML::ENGINE.yamler = 'psych'

#======================================================================================================================#



#======================================================================================================================#

class LogEntry
  fattr :date, :message
  

#----------------------------------------------------------------------------------------------------------------------#  

  def initialize(message)
    @date = Time.new
    @message = message.dup
  end
end


#======================================================================================================================#

class Issue
  fattr :id, :created, :type, :title, :description, :status
  attr_accessor :history
  

#----------------------------------------------------------------------------------------------------------------------#  

  def initialize
    @history = []
  end


#----------------------------------------------------------------------------------------------------------------------#  

  def self.createNewIssue(title, type="bug")
    newIssue = Issue.new
    newIssue.id = SecureRandom.hex.force_encoding("UTF-8")
    newIssue.created  = Time.new
    newIssue.title = title
    newIssue.status = "open"
    newIssue.type = type
    newIssue.history = [LogEntry.new("Issue created")]
    newIssue
  end


#----------------------------------------------------------------------------------------------------------------------#  

  def copy_from(a_issue)
    self.class.fattrs.each do |a|
      a_issue.send(a) && self.send(a, a_issue.send(a).dup)
    end
  end  
      

#----------------------------------------------------------------------------------------------------------------------#  

  def log(message)
    @history ||= []
    @history << LogEntry.new(message)
  end
  
      

#----------------------------------------------------------------------------------------------------------------------#  
  
  def format_verbose
    puts PrettyComment.separator
    puts PrettyComment.comment("#{@id[0,6]} #{@type.capitalize} (#{@status})  #{@created.to_s[0,16]}")
    puts PrettyComment.comment("")
    puts PrettyComment.format_line(@title, "#", false, "#")
    
    if @description
      puts PrettyComment.sub_heading("Description:")
      @description.split("\n").each do |l|
        puts PrettyComment.format_line(@description, "#", false, "#")
      end
    end
    
    if @history && @history.count > 0
      puts PrettyComment.sub_heading("Log:")
      @history.each { |l| puts PrettyComment.format_line("#{l.message}", "# #{l.date.to_s[0,16]}", true, "#", "#") }
    end
    
    puts PrettyComment.separator
    puts
    puts
  end


#----------------------------------------------------------------------------------------------------------------------#

  def format_list
    puts PrettyComment.format_line(@title, "#{short_id} (#{@type[0,1].capitalize})", true)
  end


#----------------------------------------------------------------------------------------------------------------------#  

  def edit_description
    file = Tempfile.new('issues')
    file.write(@description)
    file.close
    system("$EDITOR #{file.path}")

    file.open
    new_description = file.read

    if new_description != @description
      @description = new_description.dup
      return true
    end
    
    return false
  end


#----------------------------------------------------------------------------------------------------------------------#  

  def edit_all
    edit_file = file = Tempfile.new('issues')
    file.write(self.to_yaml)
    file.close
    
    system("$EDITOR #{file.path}")

    file.open
    
    if (file.read != self.to_yaml)
      new_issue = YAML::load_file(file.path)
      self.copy_from(new_issue)
      return true
    end 
    
    return false
  end


#----------------------------------------------------------------------------------------------------------------------#  

  def short_id
    @id[0,6]
  end
    
end


#======================================================================================================================#
#                                                   Main Program Logic
#======================================================================================================================#

class IssuesDb
  fattr :issues_array
  
#----------------------------------------------------------------------------------------------------------------------#

  def initialize(database_file)
    @database_file = database_file
    @issues_array = FileTest.exists?(database_file) && YAML.load_file(database_file) || []
  end
  

#----------------------------------------------------------------------------------------------------------------------#

  def select_issues(&select_proc)
    return @issues_array.select(&select_proc)
  end
  

#----------------------------------------------------------------------------------------------------------------------#

  def select_issue(&select_proc)
    result = select_issues(&select_proc)
    
    if result.count == 1
      return result[0]
  
    elsif result.count > 1
      puts "Found more than one issue that match this query:"
      result.each{|i| puts("#{i.id} #{i.title}")}
      exit
    
    else
      puts "Error: No issue found for query."
      exit
    end

    nil
  end  


#----------------------------------------------------------------------------------------------------------------------#

  def has_issue(issue_id)
    @issues_array.any? { |issue| issue.id.start_with?(issue_id) }
  end
  

#----------------------------------------------------------------------------------------------------------------------#

  def save_db()
    FileTest.exists?('.issues') || Dir.mkdir('.issues')
    File.open(@database_file, 'w' ) { |out| YAML.dump(@issues_array, out) }
  end
  

#----------------------------------------------------------------------------------------------------------------------#

  def determine_issue_type(opts)
    issue_types = %w{bug improvement task}
    issue_type = nil
    issue_types.each { |t| opts[t.to_sym] == true && issue_type = t }
    
    issue_type && (return issue_type)

    case opts[:title]
    when /\b(improve|implement)/i
      "improvement"
    when /\b(fix|bug|crash)/i
      "bug"
    else
      "task"
    end
  end


#----------------------------------------------------------------------------------------------------------------------#

  def create_issue(opts)
    type = determine_issue_type(opts)
    new_issue = Issue.createNewIssue(opts[:title], type)
    @issues_array << new_issue
    save_db()  
    puts "Created issue #{new_issue.short_id} #{new_issue.title}"
  end


#----------------------------------------------------------------------------------------------------------------------#

  def list_issues(opts)
    if opts[:issue_id]
      list_issue(opts[:issue_id])
    else
      list_proc = opts[:verbose] ? "format_verbose" : "format_list"

      issue_types = %w{bug improvement task}
      did_select_issue_types = opts.any? { |key,value| issue_types.include?(key.to_s.chomp('s')) && value == true }
      did_select_issue_types && issue_types.delete_if { |issue_type| opts["#{issue_type}s".to_sym] == false }
      
      status_regex = opts[:all] ? /^(open|resolved|duplicate|wontfix)/ : /^open$/
            
      issues = @issues_array.select { |issue| (status_regex =~ issue.status) && issue_types.include?(issue.type) } 
      issues.each {|issue| issue.method(list_proc).call}
    end
  end


#----------------------------------------------------------------------------------------------------------------------#

  def list_issue(issue_id)
    issue = select_issue {|i| i.id.start_with?(issue_id) }
    issue.format_verbose()
  end


#----------------------------------------------------------------------------------------------------------------------#

  def resolve_issues(opts)
    issue_id = opts[:issue_id]
    resolved_issue = select_issue{|i| i.id.start_with?(issue_id) && i.status == "open"}

    duplicate_of_id = opts[:cmd] == "duplicate" && select_issue {|i| i.id.start_with?(opts[:duplicate_of_id]) }.id

    status, message = 
    case opts[:cmd]
    when "resolve"
      ["resolved", "Resolved"]
    when "wontfix"
      ["wontfix", "Won't fix"]
    when "duplicate"
      ["duplicate(#{duplicate_of_id})", "Duplicate"]
    end

    resolved_issue.status = status
    resolved_issue.log "Changed status to #{status}"
    
    message = "#{message} issue #{resolved_issue.short_id} #{resolved_issue.title}"
    puts message
    

    save_db()
    opts[:commit] && exec("git commit -a -m \"#{message}\"")
  end


#----------------------------------------------------------------------------------------------------------------------#

  def delete_issues(opts)
    delete_issues = []

    opts[:issue_ids].each do |issue_id|
      delete_issues << select_issue{|i| i.id.start_with?(issue_id)}
    end
    
    puts "Ok to delete issues: "
    delete_issues.each { |issue| puts "#{issue.short_id} \"#{issue.title}\"" }
    puts "[y/N]"
    
    answer = STDIN.getch
    
    if /y/i =~ answer
      @issues_array -= delete_issues
      save_db()
      
      if delete_issues.count == 1
        puts "Removed issue #{delete_issues[0].short_id} \"#{delete_issues[0].title}\" from database."
      else
        puts "Removed issues "
        delete_issues.each { |issue| puts "#{issue.short_id} \"#{issue.title}\"" }
        puts "from database."
      end
    end

  end


#----------------------------------------------------------------------------------------------------------------------#

  def edit_issue(opts)
    issue_id = opts[:issue_id]
    issue = select_issue { |i| i.id.start_with?(issue_id) } 
  
    did_change_issue = false
  
    if (opts[:description])
      did_change_issue = issue.edit_description && issue.log("Edited description")
    else
      did_change_issue = issue.edit_all && issue.log("Edited issue")
    end
   
    did_change_issue && save_db()
  end


#----------------------------------------------------------------------------------------------------------------------#

  def set_type(opts)
    new_type = opts[:new_type]
    opts[:issue_ids].each do |issue_id|
      issue = select_issue { |i| i.id.start_with?(issue_id) }
      if issue.type != new_type
        issue.type = new_type
        issue.log("Changed typed to #{issue.type}")
        issue.format_list
      else
        puts "Issue #{issue.short_id} already of type #{issue.type}."
      end
    end
    
    save_db()
  end


#----------------------------------------------------------------------------------------------------------------------#


end


#======================================================================================================================#
# Command Line Parsing
#======================================================================================================================#

def get_issue_ids(num_ids, usage)
  result = []
  count = num_ids >= 0 ? num_ids : ARGV.count
  
  begin
    count.times do
      ARGV.count > 0 && /^\h{1,32}$/ =~ ARGV[0] || raise 
      result << ARGV.shift 
    end

    result.count == 1 && num_ids > 0 ? result[0] : result

  rescue
    abort("Usage: issues #{usage}")
  end
end


#----------------------------------------------------------------------------------------------------------------------#

EXECUTABLE_NAME=File.basename($0)
DATABASE_NAME= ".issues/" << EXECUTABLE_NAME << ".yaml"

SUB_COMMANDS = {
  "list"      => "list issues",
  "create"    => "create a new issue",
  "resolve"   => "set status of issue to \"resolved\"",
  "wontfix"   => "set status of issue to \"won't fix\"",
  "duplicate" => "mark issue as duplicate of another issue",
  "edit"      => "edit an existing issue",
  "delete"    => "delete an issue",
  "set-type"  => "set the type of an issue"}

LeftFieldLength = 
  SUB_COMMANDS.collect { |key, value| key.length }.max
  
SubCommandHelp =
  SUB_COMMANDS.collect {|key,value| "  #{key.ljust(LeftFieldLength)}  #{value}"}.join("\n")

  
global_opts = Trollop::options do
  banner <<-EOL
issues: lightweight distributed issue management.
  
Usage:
------
issues [<command>] [<options] [<args>]
  
Commands are:
-------------
#{SubCommandHelp}

Global Options:
---------------
  EOL
  stop_on SUB_COMMANDS.keys
end


cmd = ARGV.shift # get the subcommand
cmd ||= 'list'

cmd_opts = {}

if cmd == 'list'
  cmd_opts = 
    Trollop::options do
      opt :all, "list all issues",            :short => 'a'
      opt :newest, "list newest issues first"
      opt :oldest, "list oldest issues first"
      opt :verbose, "verbose list of issues", :short => 'v'
      opt :bugs, "list bugs",                 :short => 'b'
      opt :improvements, "list improvements", :short => 'i'
      opt :tasks, "list tasks",               :short => 't'
    end
    
    ARGV.count > 0 && cmd_opts[:issue_id] = get_issue_ids(1, "list ID")

elsif cmd == "create"
  cmd_opts = 
    Trollop::options do
      opt :bug, "create a bug", :short => 'b'
      opt :improvement, "create an improvement", :short => 'i'
      opt :task, "create a task", :short => 't'
    end
  cmd_opts[:title] = ARGV.shift || Trollop::die( "Please enter a title for the new issue!")


elsif cmd == "resolve" || cmd == "wontfix" || cmd == "duplicate"
  cmd_opts = 
    Trollop::options do
      opt :commit, "do a git commit", :short => 'c'
    end
    
  if cmd == "duplicate"
    cmd_opts[:issue_id], cmd_opts[:duplicate_of_id] = get_issue_ids(2, "duplicate ID(issue) ID(duplicate of)")
  else
    cmd_opts[:issue_id] = get_issue_ids(1, "#{cmd} [-c] ID")
  end
  
  
elsif cmd == "edit"
  cmd_opts = 
    Trollop::options do
      opt :description, "edit the issue description", :short => 'd'
    end
  cmd_opts[:issue_id] = get_issue_ids(1, "edit ID") 


elsif cmd == "set-type"
  Trollop::options do
    banner <<-EOL
Usage:
------
issues set-type {bug|improvement|task} ID

Options:
--------
EOL
  end

  new_type = ARGV.shift
  %w{bug improvement task}.include?(new_type) || Trollop::die("Please specify one of [bug, improvement, task] as new issue type")
  cmd_opts[:new_type] = new_type
  ARGV.count > 0 && cmd_opts[:issue_ids] = get_issue_ids(-1, "set-type {bug|improvement|task} ID")


elsif cmd == "delete"
  cmd_opts[:issue_ids] = get_issue_ids(-1, "#{cmd} ID")

else
  Trollop::die "unknown command #{cmd.inspect}"
end


cmd_opts[:cmd] = cmd 


#======================================================================================================================#
#                                                         Main
#======================================================================================================================#

Issues = IssuesDb.new(DATABASE_NAME)

case cmd
  when "create" 
    Issues.create_issue(cmd_opts)
  when "list" 
    Issues.list_issues(cmd_opts)
  when "resolve", "wontfix", "duplicate"
    Issues.resolve_issues(cmd_opts)
  when "edit"
    Issues.edit_issue(cmd_opts)
  when "set-type"
    Issues.set_type(cmd_opts)
  when "delete"
    Issues.delete_issues(cmd_opts)
end


#======================================================================================================================#

